﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.IO
{
    using System;
    using System.Diagnostics.CodeAnalysis;
    using System.IO;

    /// <summary>Transparently encodes or decodes data to/from base64.</summary>
    /// <remarks>
    /// <para>When data is written to this stream then it is first encoded and the encoded form is then written to the
    /// underlying stream that was passed to the constructor. A call to any of the Read methods of this stream takes
    /// data read from the underlying stream. The data is then decoded and the decoded form is then returned. For both
    /// cases a separate buffer of the size specified at construction is used, so calls to the Read/Write methods of
    /// this class do not directly translate to calls to the equivalent methods of the underlying stream.</para>
    /// <para>This class aims to be a fully conformant implementation of
    /// <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a> 6.8.</para>
    /// <para><b>Caution</b>: When data is written to this stream, then <see cref="Stream.Dispose()"/> <b>must</b> be
    /// called in the end, see <see cref="Dispose"/> for more information.</para>
    /// </remarks>
    /// <threadsafety static="true" instance="false"/>
    public sealed class Base64Stream : BufferStream
    {
        private const int BitsPerBytePrimeFactor = 2;
        private const int BitsPerByte = BitsPerBytePrimeFactor * BitsPerBytePrimeFactor * BitsPerBytePrimeFactor;
        private const int BitsPerEncodedByte = 6;

        private const int BitOffset = BitsPerByte - BitsPerEncodedByte;
        private const int EncodedByteMask = (1 << BitsPerEncodedByte) - 1;

        /// <summary>The following series of constants calculate the least common multiple of <see cref="BitsPerByte"/>
        /// and <see cref="BitsPerEncodedByte"/>.</summary>
        /// <remarks>Since the only prime factor of <see cref="BitsPerByte"/> is <see cref="BitsPerBytePrimeFactor"/>,
        /// we can establish cancel factors by dividing <see cref="BitsPerEncodedByte"/> by
        /// <see cref="BitsPerBytePrimeFactor"/> repeatedly until we receive a non-zero remainder. The least common
        /// multiple is then the product of <see cref="BitsPerByte"/> and <see cref="BitsPerEncodedByte"/> divided by
        /// the cancel factors.</remarks>
        private const int CancelFactor1 = BitsPerEncodedByte % BitsPerBytePrimeFactor == 0 ? BitsPerBytePrimeFactor : 1;
        private const int CancelFactor2 =
            (BitsPerEncodedByte / CancelFactor1) % BitsPerBytePrimeFactor == 0 ? BitsPerBytePrimeFactor : 1;

        private const int LeastCommonMultiple = BitsPerByte * BitsPerEncodedByte / CancelFactor1 / CancelFactor2;
        private const int OriginalBytesPerGroup = LeastCommonMultiple / BitsPerByte;
        private const int EncodedBytesPerGroup = LeastCommonMultiple / BitsPerEncodedByte;

        private const int CrlfLength = 2;
        private const int MaxLineLength = 78; // Includes CRLF
        private const int MaxCharactersPerLine = MaxLineLength - CrlfLength;
        private const int MaxEncodedCharactersPerLine =
            MaxCharactersPerLine - (MaxCharactersPerLine % EncodedBytesPerGroup);

        private const int MaxEncodedLineLength = MaxEncodedCharactersPerLine + CrlfLength;

        private static readonly byte[] decodeTable = new byte[]
        {
         // 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x00
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x10
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3e, 0xff, 0xff, 0xff, 0x3f, // 0x20
            0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x30
            0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, // 0x40
            0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x50
            0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, // 0x60
            0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x70
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x80
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0x90
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0xa0
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0xb0
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0xc0
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0xd0
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // 0xe0
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff  // 0xf0
        };

        private static readonly byte[] encodeTable = new byte[]
        {
         // 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
            0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, // 0x00
            0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, // 0x10
            0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, // 0x20
            0x77, 0x78, 0x79, 0x7a, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x2b, 0x2f  // 0x30
        };

        private byte previousDecodedByte = byte.MaxValue;
        private int previousDecodedByteShift;

        private int currentColumn;
        private byte previousOriginalByte = byte.MaxValue;
        private int currentOriginalByteShift;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Initializes a new instance of the <see cref="Base64Stream"/> class by calling
        /// <see cref="Base64Stream(Stream, int)"/>(<paramref name="stream"/>, 1024).</summary>
        public Base64Stream(Stream stream) : this(stream, Settings.InMemoryStreamBufferSize)
        {
        }

        /// <summary>Initializes a new instance of the <see cref="Base64Stream"/> class.</summary>
        /// <param name="stream">The stream from/to which encoded data is read/written.</param>
        /// <param name="bufferSize">The size of the internal read and write buffers in bytes.</param>
        /// <exception cref="ArgumentNullException"><paramref name="stream"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="bufferSize"/> is less than or equal to 0.
        /// </exception>
        public Base64Stream(Stream stream, int bufferSize) : base(stream, bufferSize)
        {
        }

        /// <summary>See <see cref="Stream.Read(byte[], int, int)"/>.</summary>
        public sealed override int Read(byte[] buffer, int offset, int count)
        {
            this.AssertNotDisposed();
            BufferHelper.AssertValidRange(buffer, "buffer", offset, "offset", count, "count");

            // Letting the code run as unchecked doubles the performance in the device emulator. On the desktop
            // performance does not seem to be affected.
            unchecked
            {
                ReadBuffer readBuffer = this.ReadBuffer;
                int index = offset;
                int pastEnd = offset + count;
                byte currentDecodedByte;

                // Only try to get more encoded data if we haven't yet copied anything into buffer
                while ((index < pastEnd) && ((readBuffer.Index < readBuffer.Count) ||
                    ((index == offset) && readBuffer.Fill())))
                {
                    currentDecodedByte = decodeTable[readBuffer[readBuffer.Index++]];

                    if (currentDecodedByte != 0xff)
                    {
                        if (this.previousDecodedByteShift != 0)
                        {
                            // The expression may yield a value greater than byte.MaxValue but only the least
                            // significant 8 bits are of interest. For that purpose, casting to byte is sufficient in
                            // unchecked code. In checked code however, we'd need to mask out the most significant bits
                            // first.
                            buffer[index++] = (byte)((this.previousDecodedByte << this.previousDecodedByteShift) +
                                (currentDecodedByte >> (BitsPerEncodedByte - this.previousDecodedByteShift)));
                        }

                        this.previousDecodedByte = currentDecodedByte;
                        this.previousDecodedByteShift = (this.previousDecodedByteShift + BitOffset) % BitsPerByte;
                    }
                }

                return index - offset;
            }
        }

        /// <summary>See <see cref="Stream.Write(byte[], int, int)"/>.</summary>
        public sealed override void Write(byte[] buffer, int offset, int count)
        {
            this.AssertNotDisposed();
            BufferHelper.AssertValidRange(buffer, "buffer", offset, "offset", count, "count");

            unchecked
            {
                WriteBuffer writeBuffer = this.WriteBuffer;
                int pastEnd = offset + count;
                byte currentOriginalByte = 0xff;
                byte encodedByte;

                while ((offset < pastEnd) && ((writeBuffer.Count < writeBuffer.Capacity) || writeBuffer.Flush()))
                {
                    switch (this.currentColumn)
                    {
                        case MaxEncodedLineLength - 2:
                            encodedByte = (byte)'\r';
                            break;
                        case MaxEncodedLineLength - 1:
                            encodedByte = (byte)'\n';
                            break;
                        default:
                            if (this.currentOriginalByteShift != BitsPerEncodedByte)
                            {
                                currentOriginalByte = buffer[offset++];
                            }

                            encodedByte = encodeTable[(
                                (this.previousOriginalByte << (BitsPerEncodedByte - this.currentOriginalByteShift)) +
                                (currentOriginalByte >> (this.currentOriginalByteShift + BitOffset))) &
                                EncodedByteMask];
                            this.currentOriginalByteShift = (this.currentOriginalByteShift + BitOffset) % BitsPerByte;
                            this.previousOriginalByte = currentOriginalByte;
                            break;
                    }

                    writeBuffer[writeBuffer.Count++] = encodedByte;
                    this.currentColumn = (this.currentColumn + 1) % MaxEncodedLineLength;
                }
            }
        }

        /// <summary>Gets the length in bytes that will result when data with the length
        /// <paramref name="originalLength"/> (in bytes) is encoded.</summary>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="originalLength"/> is negative.</exception>
        public static int GetEncodedLength(int originalLength)
        {
            if (originalLength < 0)
            {
                throw new ArgumentOutOfRangeException("originalLength", ExceptionMessages.NonnegativeNumberRequired);
            }

            var encodedBytes =
                (originalLength + OriginalBytesPerGroup - 1) / OriginalBytesPerGroup * EncodedBytesPerGroup;
            return encodedBytes + (encodedBytes / MaxEncodedCharactersPerLine * CrlfLength);
        }

        /// <summary>Gets the maximum length in bytes that will result when data with the length
        /// <paramref name="encodedLength"/> (in bytes) is decoded.</summary>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="encodedLength"/> is negative.</exception>
        public static int GetMaxOriginalLength(int encodedLength)
        {
            if (encodedLength < 0)
            {
                throw new ArgumentOutOfRangeException("encodedLength", ExceptionMessages.NonnegativeNumberRequired);
            }

            return encodedLength * MaxEncodedCharactersPerLine / (MaxEncodedCharactersPerLine + CrlfLength) *
                OriginalBytesPerGroup / EncodedBytesPerGroup;
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="Stream.Dispose(bool)"/>.</summary>
        /// <remarks>If <paramref name="disposing"/> is <c>true</c> then the last encoded byte and any required padding
        /// is written into the underlying stream.</remarks>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Dispose() must never throw.")]
        protected sealed override void Dispose(bool disposing)
        {
            try
            {
                if (disposing && !this.IsDisposed)
                {
                    // Unfortunately, some of what follows is duplicated from the Write method. This could only be
                    // avoided at the cost of sacrificing quite a bit of execution speed in the emulator (and thus
                    // on devices).
                    bool isPadding = false;

                    // If necessary, the following code adds the final byte and the padding required by the RFC. That
                    // is, it ensures that the final group is a complete group of 4 characters as all the previous
                    // groups. Moreover, the maximum line length is also expressed as a number of groups of 4 encoded
                    // characters (see MaxGroupCountPerLine). So, a group is never spread over two lines and
                    // consequently we don't need to worry about inserting "\r\n" sequences here (as in the Write
                    // method).
                    // Note: Below, unlike in the Write method, we don't use a local variable for the WriteBuffer object
                    // because doing so might unnecessarily allocate such an object, even when nothing was ever written
                    // to this stream.
                    while ((this.currentOriginalByteShift != 0) &&
                        ((this.WriteBuffer.Count < this.WriteBuffer.Capacity) || this.WriteBuffer.Flush()))
                    {
                        this.WriteBuffer[this.WriteBuffer.Count++] = isPadding ? (byte)'=' : encodeTable[(
                            this.previousOriginalByte << (BitsPerEncodedByte - this.currentOriginalByteShift))
                                & EncodedByteMask];
                        this.currentOriginalByteShift = (this.currentOriginalByteShift + BitOffset) % BitsPerByte;
                        isPadding = true;
                    }
                }
            }
            catch
            {
            }
            finally
            {
                base.Dispose(disposing);
            }
        }
    }
}
